package redislock

import (
	"context"
	"errors"
	"math/rand"
	"time"

	"gitee.com/zawei/yggdrasil/pkg/lib/redis"
)

var (
	// ErrNotObtained is returned when a lock cannot be obtained.
	ErrNotObtained = errors.New("redislock: not locked")

	// ErrLockNotHeld is returned when trying to release an inactive lock.
	ErrLockNotHeld = errors.New("redislock: lock not held")
)

type RedisLocker struct {
	*Config
	cli *redis.Redis
}

func newRedisLocker(config *Config) *RedisLocker {
	return &RedisLocker{
		Config: config,
		cli:    config.Redis.Build(),
	}
}

func NewLocker(config *Config) *RedisLocker {
	return config.Build()
}

func (l *RedisLocker) backoff(retry int) time.Duration {
	if retry < 0 {
		retry = 0
	}

	backoff := l.MinBackoff << uint(retry)
	if backoff > l.MaxBackoff || backoff < l.MaxBackoff {
		backoff = l.MaxBackoff
	}

	if backoff == 0 {
		return 0
	}
	return time.Duration(rand.Int63n(int64(backoff)))
}

func (l *RedisLocker) WithOptions(opts ...LockerOption) *RedisLocker {
	locker := *l
	config := *l.Config
	locker.Config = &config
	for _, o := range opts {
		o(&locker)
	}
	return &locker
}

func (l *RedisLocker) Lock(ctx context.Context, key string, ops ...LockOption) (Lock, error) {
	lockImp := &lock{
		unlocked:   make(chan struct{}, 1),
		ttl:        l.TTL,
		cli:        l.cli.UniversalClient,
		key:        key,
		value:      l.uuidGen(),
		autoExtend: true,
	}
	lockImp.apply(ops...)
	retry := 0
	backoff := time.NewTimer(l.backoff(retry))
	defer backoff.Stop()
	for {
		select {
		case <-backoff.C:
			if b, err := lockImp.lock(); err != nil {
				//l.logger.Warn("Fault to lock", map[string]interface{}{
				//	"error": err,
				//	"key":   key,
				//})
				return nil, err
			} else if b {
				if lockImp.autoExtend {
					go lockImp.extendTTL()
				}
				return lockImp, nil
			}
			retry++
			backoff.Reset(l.backoff(retry))
		case <-ctx.Done():
			return nil, ErrNotObtained
		}
	}
}

func (l *RedisLocker) TryLock(key string, ops ...LockOption) (Lock, error) {
	lockImp := &lock{
		unlocked:   make(chan struct{}, 1),
		ttl:        l.TTL,
		cli:        l.cli.UniversalClient,
		key:        key,
		value:      l.uuidGen(),
		autoExtend: true,
	}
	lockImp.apply(ops...)
	if b, err := lockImp.lock(); err != nil {
		return nil, err
	} else if !b {
		return nil, ErrNotObtained
	} else {
		if lockImp.autoExtend {
			go lockImp.extendTTL()
		}
		return lockImp, nil
	}
}
